{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#远程桌面app"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "本章做一个远程桌面app，依然和网络相关。我们用“真正的”应用层协议进行通信，解决一个复杂的问题。\n",
    "\n",
    "<!-- TEASER_END-->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "简单介绍一下：首先我们的目的是实现一个经典的桌面应用——远程连接，允许用户通过网络操纵其他电脑。这类应用在技术支持和远程协助中使用广泛。\n",
    "\n",
    "其次，介绍两个术语：主机（*host* machine）是远程控制者（运行远程控制服务器），客户机（*client*）是主机控制的系统。远程系统管理基本上就是这样的用户交互过程，主机把客户机当作代理来使用。\n",
    "\n",
    "因此，关键的两个步骤就是：\n",
    "\n",
    "- 收集客户机用户的输入（如鼠标和键盘动作），并应用到主机\n",
    "- 从主机向客户机发送任何输出（最常见的是截屏，还有声频、视频等）\n",
    "\n",
    "远程桌面就是这两个步骤的不断重复。很多商业软件都会实现这些功能，有一些甚至允许运行视频游戏——通过图形和游戏控制器加速。我们准备实现的一些功能如下：\n",
    "\n",
    "- 用户的输入只支持鼠标点击或Tab切换\n",
    "- 输出只有屏幕截图，因为通过网络抓取声音比较复杂\n",
    "- 主机只支持Windows平台，任何桌面版本都行。客户端没有限制，因为Kivy app哪儿都可以运行\n",
    "\n",
    "最后一条实在遗憾，因为不同的系统截屏和模拟点击行为的API不同，我们只能选择最流行的系统来实现。其他平台的支持以后会实现，原理都一样。\n",
    "\n",
    ">中国用户可以忽略这条：如果没Windows，自己找虚拟机安装一个。Mac上也可以用Parallers安装，就是要换点钱。\n",
    "\n",
    "本章教学大纲如下：\n",
    "\n",
    "- 用Python的Flask微框架写一个HTTP服务器\n",
    "- 用PIL（Pillow）实现截屏\n",
    "- 用WinAPI功能模拟Windows点击\n",
    "- 做一个简单的JavaScript客户端原型，用它来测试\n",
    "- 做一个基于Kivy的HTTP客户端app连接到远程桌面服务器"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "##服务器"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为了方便测试和使用，我们希望用容易实现的应用层协议做服务器。我们选择HTTP，要容易实现和简单测试，需要具体两个特征：\n",
    "\n",
    "- 支持很多特性，包括服务器端和客户端。HTTP是最流行的协议，完全符合。\n",
    "- 与其他协议不同，使用HTTP协议，我们可以用JavaScript轻易写出一个可以在浏览器上运行的客户端。这和本书的主题关系不大，但是依然是一个流行的做法\n",
    "\n",
    "建服务器的模块，我们用Flask，Django更流行，不过太大材小用了。要安装Flask，直接用pip安装即可：\n",
    "\n",
    "**pip install Flask**\n",
    "\n",
    "简单高效，完全开源，[文档](https://github.com/mitsuhiko/flask)详细，推荐学习一下。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###Flask服务器"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "服务器通常就是由一系列绑定到不同URL的handler组成的。这些绑定通常叫做路由（*routing*）。Flask可以非常容易的实现这些功能。我们建立一个网页的服务器`server.py`："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from flask import Flask\n",
    "app = Flask(__name__)\n",
    "\n",
    "@app.route('/')\n",
    "def index():\n",
    "    return 'Hello, Flask'\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    app.run(host='0.0.0.0', port=7080, debug=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在Flask里面，路由是以修饰器方式实现的，如` @app.route('/')`，在URL比较少的时候这么做很方便。\n",
    "\n",
    "'/'路由是服务器域名，对应一个IP地址。运行`server.py`后，在浏览器打开`http://127.0.0.1:7080 `，看到`Hello, Flask`说明服务器ok了。\n",
    "\n",
    "![flaskserver](kbpic/5.1flaskserver.png)\n",
    "\n",
    "这里，`app.run()`里面的参数`0.0.0.0`不是一个有效的IP地址，不能通过正常访问。服务器绑定这个IP表示我们的app会监听所有IPV4接口——也就是说，从任何一个可用的IP发出的请求都会得到相应。\n",
    "\n",
    "这和默认设置（localhost，127.0.0.1）不同。localhost的IP只允许监听同一个机器传来的请求。因此，这个IP适合调试和测试用。但是，当我们发布产品后，`0.0.0.0`就是面向世界，开张圣听。不过，注意这不会自动绕过路由器；它可以在你的局域网工作，但是在公网工作可能需要其他配置。\n",
    "\n",
    "还有，记得设置防火墙策略，因为它需要满足应用层设置的优先级。\n",
    "\n",
    ">**端口选择**\n",
    "\n",
    ">用哪个端口并不重要，重要的是服务器和客户端要用同一个端口，无论是一个浏览器还是一个Kivy app。\n",
    "\n",
    ">还要注意，几乎所有的系统中1024以下的端口一般只能由授权用户使用（root或admin）。而且很多端口已经分配了固定用途，所以建议选择1024以上的端口。\n",
    "\n",
    ">默认的HTTP端口是80，默认端口通常不需要指定，`http://www.w3.org`和`http://www.w3.org:80/`是一样的。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "你可能发现Python开发实在太容易了——几行代码就可以运行一个服务器。不过，不是样样都很简单，有些事情还不能立竿见影。\n",
    "\n",
    "这是Python的优势：如果你要实现一些不太复杂的功能，试试Python吧，通常都会取得好效果。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##服务器新功能——截屏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "协议和服务器实现之后，我们来做截屏和模拟点击功能。这里仅实现Windows的实现，Mac和Linux支持可以做练习。\n",
    "\n",
    "PIL可以实现，用`PIL.ImageGrab.grab()`就行，我们把截图保存为RGB格式。之后就是把图片接到Flask上，这样就可以通过HTTP传送了。\n",
    "\n",
    "PIL已经不再维护了，现在有Pillow实现PIL，直接用pip安装即可，具体参阅[文档](http://pillow.readthedocs.org/)。\n",
    "\n",
    "**pip install Pillow**\n",
    "\n",
    "实现的代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from flask import send_file\n",
    "from PIL import ImageGrab\n",
    "from StringIO import StringIO\n",
    "# # more compatible\n",
    "# try:\n",
    "#     from StringIO import StringIO # python2\n",
    "# except ImportError:\n",
    "#     from io import BytesIO as # python3\n",
    "\n",
    "@app.route('/desktop.jpeg')\n",
    "def desktop():\n",
    "    screen = ImageGrab.grab()\n",
    "    buf = StringIO()\n",
    "    screen.save(buf, 'JPEG', quality=75)\n",
    "    buf.seek(0)\n",
    "    return send_file(buf, mimetype='image/jpeg')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里`StringIO`是把内容存在内存，不是磁盘上。使用API处理临时数据的时候这种“虚拟文件”很有用。本例中，我们不想要保存截屏内容，就是一个临时文件。如果连续下载文件到磁盘上效率很低，不如直接放到内存里，用完就释放。\n",
    "\n",
    "代码很简单，就是用`PIL.ImageGrab.grab()`截屏叫`screen`，然后用`screen.save()`保存为JPEG格式，这样省流量一些，最后用MIME type形式`'image/jpeg'`发送到Flask，这样就可以直接现在在浏览器上了。\n",
    "\n",
    ">为了实现更好的速度，用比分辨率的图片更合适，因为每一个帧都要截图是很费流量的。\n",
    "\n",
    ">这里又一次充分证明了一点：验证新概念或市场研究时，快速原型是多么高效。\n",
    "\n",
    "`buf.seek(0)`是为了倒回（*rewind*）`StringIO`实例；否则，程序就到了数据的最后，不会给`send_file()`发送任何数据了。\n",
    "\n",
    "现在就可以测试效果了，打开`http://127.0.0.1:7080/desktop.jpeg`就会看到当前屏幕的截图了。\n",
    "\n",
    "![screenshot](kbpic/5.2screenshot.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "这里的路由很有意思**\"desktop.jpeg\"**，URL的最后是文件曾经在服务器工具里面是一种习惯，像**Personal Home\n",
    "Page (PHP)**，一种适合做简单的网站的简单编程语言。其实这里并没有路径的概念，你实际上只要把文件的名称输入地址栏然后从服务器获得它。\n",
    "\n",
    "这么做是很不安全的行为，远程连接者可以看到服务器的配置，比如`'/../../etc/passwd'`输入地址栏就可以看到密码了，之后的各种木马病毒像Trojans木马（后门）就来了。\n",
    "\n",
    "Python的网页服务器都经历过这些教训。你也可以这么写，但是强烈不推荐，这样太危险了。另外，Python的模块通常默认也不会使用这些配置。\n",
    "\n",
    "今天，从文件系统直接获取文件的事情并不是没有，但主要是用于静态文件。另外，有时我们也会把动态网页（如`/index.html`，`/desktop.jpeg`等）也写成文件名的形式是为了让用户更容易明白这些URL的作用。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###模拟点击行为"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "截屏部分完成之后，我们需要实现的功能就是鼠标点击，用WinAPI可以实现，不过很麻烦，我们用Python的ctypes模块来做。\n",
    "\n",
    "首先我们需要从URL或者点击的坐标。我们用`GET`方式来实现：`/click?x=100&y=200`，这种方法容易在浏览器测试，不像`POST`和其他HTTP方式需要其他工具来测试。\n",
    "\n",
    "Flask支持这种URL带参数的方式："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from flask import request\n",
    "\n",
    "@app.route('/click')\n",
    "def click():\n",
    "    try:\n",
    "        x = int(request.args.get('x'))\n",
    "        y = int(request.args.get('y'))\n",
    "    except TypeError:\n",
    "        return 'error: expecting 2 ints, x and y'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "建立原型的时候在可能出错的地方加异常处理代码是必要的，因为经常会忘记或发送了不合理的参数，所以我们要做异常处理，所有我们需要检查`GET`请求的参数是否可用。调试的时候如果出现错误就会显示信息，这样就知道问题出在哪里了。\n",
    "\n",
    "有了点击的坐标之后，我们就要用WinAPI来调用它们。这里需要两个函数都在`user32.dll`：`SetCursorPos()`是设置鼠标光标的位置，`mouse_event()`模拟鼠标的点击事件，比如按下或松开按键。\n",
    "\n",
    ">`user32.dll`这个32和你系统的32位或64位没关系。Win32 API首次出现在 Windows NT，比之后的AMD64 (x86_64)架构早7年多，之所以这Win32是为了和之前的Win16区分。\n",
    "\n",
    "`mouse_event()`的第一个参数是事件类型，一个C语言枚举类型（一组整型常量）。我们可以在Python里面定义这些常量，用常量2表示鼠标按下，4表示鼠标松开并不是很直观。可以这样："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import ctypes\n",
    "# this is the user32.dll reference\n",
    "user32 = ctypes.windll.user32\n",
    "\n",
    "MOUSEEVENTF_LEFTDOWN = 2\n",
    "MOUSEEVENTF_LEFTUP = 4"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ">Win API文档可以到Microsoft Developer Network (MSDN)去查：[`SetCursorPos()`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms648394.aspx)，[`mouse_event()`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms646260.aspx)。\n",
    "\n",
    ">限于篇幅不做详细介绍，而且也不会有到很多功能；WinAPI可谓包罗万象，内容十分丰富，感兴趣自行研究。\n",
    "\n",
    "模拟点击的代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "    @app.route('/click')\n",
    "    def click():\n",
    "    try:\n",
    "        x = int(request.args.get('x'))\n",
    "        y = int(request.args.get('y'))\n",
    "    except:\n",
    "        return 'error'\n",
    "\n",
    "    user32.SetCursorPos(x, y)\n",
    "    user32.mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)\n",
    "    user32.mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)\n",
    "    return 'done'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "代码很直白，就是设置鼠标位置，然后单击一下（按下，松开）。\n",
    "\n",
    "现在你可以启动Flask的服务器然后试试点击操作，可以在地址栏输入`http://127.0.0.1:7080/click?x=10&y=10`，如果左上角（10，10）这个位置有图标，图标会被选中。\n",
    "\n",
    "当然也可以实现一个双击行为，如果页面刷新的足够快的话（因为打开文件时屏幕变化很大，需要截取很多图片），这可能需要在另外一个设备上运行浏览器，记得修改对应的服务器IP地址。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##JavaScript客户端"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在这一章，我们将尝试一下JavaScript远程桌面客户端，因为我们用的是HTTP协议，JavaScript非常合适。这个简单的客户端可以在浏览器运行，作为Kivy客户端桌面应用的原型。\n",
    "\n",
    "如果你不熟悉JavaScript，不用担心，其语法很简单，与Python很相似。我们还要用到jQuery来处理DOM表和AJAX。\n",
    "\n",
    ">很多人可能不赞成在产品设计阶段使用jQuery，尤其是对那些性能要求高的应用。但是，要实现网页app的快速原型，jQuery还是很不错的，因为它用法简单，实现高效。\n",
    "\n",
    "做网页app，我们得把原来的**Hello, Flask**替换成。\n",
    "\n",
    "```html\n",
    "<!DOCTYPE html>\n",
    "<html>\n",
    "<head>\n",
    "    <meta charset=\"UTF-8\">\n",
    "    <title>Remote Desktop</title>\n",
    "</head>\n",
    "    <body>\n",
    "        <script src=\"//code.jquery.com/jquery-1.11.1.js\"></script>\n",
    "        <script>\n",
    "        // code goes here\n",
    "        </script>\n",
    "    </body>\n",
    "</html>\n",
    "```\n",
    "\n",
    "Flask要使用这个HTML，需要对`index()`做一点调整："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "@app.route('/')\n",
    "def index():\n",
    "    return app.send_static_file('index.html')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这和前面的`desktop()`函数是一样的，不过是从硬盘读取HTML文件再显示。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###截屏的无限循环"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在，让我们展示一个连续的屏幕显示方案：我们的程序每两秒请求一个截屏，然后立刻向用户显示出来。因为我们写的是网页应用，所有的复杂情况都由浏览器处理：用`<img>`标签加载图片显示到屏幕上。实现步骤如下：\n",
    "\n",
    "1. 删除旧的`<img>`标签，如果有的话\n",
    "2. 增加一个新的`<img>`标签\n",
    "3. 每2秒重复一次\n",
    "\n",
    "用JavaScript实现如下：\n",
    "\n",
    "```\n",
    "function reload_desktop() {\n",
    "    $('img').remove()\n",
    "    $('<img>', {src: '/desktop.jpeg?' + \n",
    "        Date.now()}).appendTo('body')\n",
    "}\n",
    "setInterval(reload_desktop, 2000)\n",
    "```\n",
    "代码解释如下：\n",
    "\n",
    "- `$()`是jQuery选择网页元素的函数，我们可以在元素上实现各种操作，如`.remove()`或`.insert()`\n",
    "- `Date.now()`返回当前时间戳，就是1970年1月1日到当前时间的毫秒数。我们用这个数据来阻止缓存，这样每次的信息就会更新了。\n",
    "\n",
    "让我们把图片的调整为适合浏览器的尺寸，去掉所有的边距。用CSS也很容易实现：\n",
    "\n",
    "```html\n",
    "<style>\n",
    "    body { margin: 0 }\n",
    "    img { max-width: 100% }\n",
    "</style>\n",
    "```\n",
    "\n",
    "应用的截图如下所示：\n",
    "\n",
    "![remotedesk](kbpic/5.3remotedesk.png)\n",
    "\n",
    "加载的时候你会发现图片在闪烁，因为在完全加载之前就立刻显示`desktop.jpeg`。还有一个问题就是下载频率是固定的，我们随意设定为两秒。由于网速的原因，用户可能还没看到图片加载完成就又改变了。\n",
    "\n",
    "这里只是原型，在Kivy设计的时候我们会纠正这个问题。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###把点击传回主机"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "下面我们要把` <img> `截图上的点击传给服务器。对`<body>`元素使用`.bind()`方法可以实现。因为我们不断的增删元素，图片上绑定的内容在更新后会丢失，而重新绑定也没必要。\n",
    "\n",
    "```\n",
    "function send_click(event) {\n",
    "    var fac = this.naturalWidth / this.width\n",
    "    $.get('/click', {x: 0|fac * event.clientX,\n",
    "               y: 0|fac * event.clientY})\n",
    "}\n",
    "$('body').on('click', 'img', send_click)\n",
    "```\n",
    "这里我们计算了点击的真实位置：通过图片缩放比例对坐标位置进行调整。\n",
    "\n",
    "$$x,y_{server} = \\frac {width_{natural}}{width_{scaled}} \\times x,y_{client}$$\n",
    "\n",
    "JavaScript里面的`0|expression`表达式是`Math.floor()`的另外一种形式，更快更精确，只是语法有点差异。\n",
    "\n",
    "然后用jQuery的`$.get()`函数把前面计算的结果发给服务器。由于我们打算立刻显示一个新截屏，所以就没有对服务器响应进行处理——如果我们最后一个动作有任何变化，都会立即显示出来。\n",
    "\n",
    "用这个远程桌面原型，我们已经可以看到桌面，执行机器上的程序了。现在，让我们用Kivy来实现这个原型，同时做些改进，增加滚动条、去掉屏幕闪烁，让它更适用于移动设备。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "##Kivy远程桌面app"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "我们可以重用上一章聊天app的一些功能。这里个应用很相似，都是由两个屏幕构成，一个屏幕是带服务器地址的登录界面。我们就把`chat.kv`文件改造成`remotedesktop.kv`文件，里面的`ScreenManager`保留。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###登录界面"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "就是下面的代码，包含三个部分——标题、输入文本框、登录按钮——位于屏幕的顶部：\n",
    "\n",
    "```yaml\n",
    "Screen:\n",
    "    name: 'login'\n",
    "    \n",
    "    BoxLayout:\n",
    "        orientation: 'horizontal'\n",
    "        y: root.height - self.height\n",
    "        \n",
    "        Label:\n",
    "            text: 'Server IP:'\n",
    "            size_hint: (0.4, 1)\n",
    "        TextInput:\n",
    "            id: server\n",
    "            text: '10.211.55.5' # put your server IP here\n",
    "        Button:\n",
    "            text: 'Connect'\n",
    "            on_press: app.connect()\n",
    "            size_hint: (0.4, 1)\n",
    "```\n",
    "只要一个输入文本框**Server IP**。其实你也可以用服务器名称，但是这需要配置，直接IP省事儿。\n",
    "\n",
    "虽然用IP地址不是很直观，但是我们暂时没更好的选择。要做一个自动发现的网络服务来避免IP地址，用着更舒服，但是也更复杂（需要一大堆技术来实现）。\n",
    "\n",
    ">这里，需要掌握基本的网络技能，比如把设备连接到路由器。这些都超出本文的范围，简单说下：\n",
    "\n",
    "> - 当你所有的设备都连接到同一网络是测试更容易\n",
    "> - 通一台设备上使用虚拟机来测试也行。这样可以避开扫描网络、输密码、连线等等操作。\n",
    "> - 要看设备的IP地址，用`ipconfig`(Windows)或`ifconfig`。公网IP不会显示出来，但局域网IP会显示。\n",
    "\n",
    "登录窗口很简单，前面已经学习过，下面让我们来实现远程桌面窗口。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###远程桌面窗口"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "这个屏幕界面应该有二维的滚动条，可以在不同屏幕上选择位置。我们还按照上一章聊天app的滚动条来实现。不同的是：这里是二维的，这次我们不想让让任何超限再触底反弹的效果以避免冲突，操作系统桌面通常不需要这些效果。\n",
    "\n",
    "这个屏幕的`remotedesktop.kv`文件如下：\n",
    "\n",
    "```yaml\n",
    "Screen:\n",
    "    name: 'desktop'\n",
    "    \n",
    "    ScrollView:\n",
    "        effect_cls: ScrollEffect\n",
    "        \n",
    "        Image:\n",
    "            id: desktop\n",
    "            nocache: True\n",
    "            on_touch_down: app.send_click(args[1])\n",
    "            size: self.texture_size\n",
    "            size_hint: (None, None)\n",
    "```\n",
    "\n",
    "在`ScrollView`里，我们`effect_cls: ScrollEffect`来禁止超限行为。如果你想要这种行为，可以不用这行。由于`ScrollEffect`不是默认存在的，需要导入：\n",
    "```\n",
    "#:import ScrollEffect kivy.effects.scroll.ScrollEffect\n",
    "```\n",
    "关键点是把`Image`的`size_hint`属性设置为`(None, None)`；否则Kivy就会自动放缩图片来适合屏幕尺寸，这样滚动条就没用了，而且在不同比例的屏幕上，桌面会失真。设置为`None`表示要是手动调节。\n",
    "\n",
    "之后，我们把`size`属性设置成`self.texture_size`。这样就可以把服务生成的`desktop.jpeg`的尺寸传递到屏幕了（这是有主机的屏幕决定的，我们不能改变）。\n",
    "\n",
    "还有`nocache: True`属性，是让Kivy取消缓存，不要保存截图的临时文件。最后，`Image`的`on_touch_down`属性。我们想传递精确的坐标值和触摸事件的其他属性，就得用`args[1]`。`args[0]`是要点击的部件，也就是图片本身（我们只有一个`Image`实例，所以不需要把它传递到事件handler）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###截屏在Kivy里的循环"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在让我们把前面这些模块集成到Python里。和JavaScript实现相反，我们不用加载图片和相关的功能，所以要写点儿代码；不过这些很容易实现，而且更容易维护。\n",
    "\n",
    "为了异步加载图片，我们要用Kivy的`Loader`类，实现思路如下：\n",
    "\n",
    "1. 当用户填好**Server IP**，在登录窗口点击**Connect**后，`RemoteDesktopApp.connect()`函数启动\n",
    "2. 它把控制传递到`reload_desktop()`，这个函数会从`/desktop.jpeg`下载图片\n",
    "3. 图片加载之后，`Loader`调用`desktop_loaded()`，把图片展示到屏幕上，然后调用下一个`reload_desktop()`。这样我们就通过异步方式实现了截屏的循环\n",
    "\n",
    "下面是`main.py`文件实现代码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.loader import Loader\n",
    "\n",
    "class RemoteDesktopApp(App):\n",
    "    def connect(self):\n",
    "        self.url = ('http://%s:7080/desktop.jpeg' % \n",
    "                    self.root.ids.server.text)\n",
    "        self.send_url = ('http://%s:7080/click?' % \n",
    "                         self.root.ids.server.text)\n",
    "        self.reload_desktop()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们保持`url`（服务器IP和`/desktop.jpeg`）和`send_url`（服务器IP和`/click`），然后传递`RemoteDesktopApp.reload_desktop()`函数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def reload_desktop(self, *args):\n",
    "    desktop = Loader.image(self.url, nocache=True)\n",
    "    desktop.bind(on_load=self.desktop_loaded)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "前面函数就是下载图片。图片下载完之后，就把图片加载到`RemoteDesktopApp.desktop_loaded()`。\n",
    "\n",
    "不用忘了用`nocache=True`禁止缓存，没有这点桌面就只会加载一次，因为URL是一样的。在JavaScript里面，我们解决这个问题是把`?timestamp`放在URL之后，在Python我们可以不这样做，因为Kivy支持缓存控制功能。\n",
    "\n",
    "然后就可以完成桌面加载程序了："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.clock import Clock\n",
    "\n",
    "def desktop_loaded(self, desktop):\n",
    "    if desktop.image.texture:\n",
    "        self.root.ids.desktop.texture = desktop.image.texture\n",
    "    \n",
    "    Clock.schedule_once(self.reload_desktop, 1)\n",
    "    \n",
    "    if self.root.current == 'login':\n",
    "        self.root.current = 'desktop'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "函数接收新图片`desktop`，更新屏幕，然后每1秒循环一次。\n",
    "\n",
    ">在第一章的时钟app里面，我们讨论过`Clock`对象的更新问题，当时是用`schedule_interval()`，类似于JavaScript的`setInterval()`，这里我们想要持续状态，类似于JavaScript的`setTimeout()`功能。\n",
    "\n",
    "现在屏幕更新就实现了，截图如下所示：\n",
    "![desktopserver](kbpic/5.4desktopserver.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###传送点击"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "远程桌面已经完成，下面我们实现点击行为，这就需要监听图片的`on_touch_down`事件，把触摸的坐标值传递到`send_click()`函数。在`remotedesktop.kv`文件里面：\n",
    "\n",
    "```yaml\n",
    "Screen:\n",
    "    name: 'desktop'\n",
    "    \n",
    "    ScrollView:\n",
    "        effect_cls: ScrollEffect\n",
    "        Image:\n",
    "            on_touch_down: app.send_click(args[1])\n",
    "            # The rest of the properties unchanged\n",
    "```\n",
    "\n",
    "在Python的`class RemoteDesktopApp`中实现`send_click()`函数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def send_click(self, event):\n",
    "    params = {'x': int(event.x),\n",
    "              'y': int(self.root.ids.desktop.size[1] - event.y)}\n",
    "    urlopen(self.send_url + urlencode(params))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里需要注意的是坐标值：在Kivy里面，`y`轴是向上的，和数学上的概念一致，而Windows和浏览器里面都是向下的。所以我们用桌面高度`event.y`来转换。\n",
    "\n",
    "另一个容易出错的就是Python的`urllib`模块，Python2和Python3的差别很大，也可以用[`requests`模块](http://python-requests.org/)，不过这里用标准模块。可以通过异常处理开解决版本问题："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "try: # python 2\n",
    "    from urllib import urlencode\n",
    "except ImportError: # python 3\n",
    "    from urllib.parse import urlencode\n",
    "    \n",
    "try: # python 2\n",
    "    from urllib2 import urlopen\n",
    "except ImportError: # python 3\n",
    "    from urllib.request import urlopen"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "虽然代码不是很简洁，但是可以实现兼容性，目前只能这样。连Guido老爹（Python创始人）现在也这么写，还能说啥呢。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###后续任务"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "现在我们的远程桌面app就搞定了，建议在局域网和网速快的时候使用。当然，还有很多问题需要解决，很多新特性可以加入，如果你感兴趣，下面是一些努力的方向：\n",
    "\n",
    "- 把鼠标行为作为单独事件处理，可以实现双击、拖拽等\n",
    "- 考虑网络延迟。如果用户网速很慢，你可以把图片质量降低来保证流畅性，给用户一个选择\n",
    "- 让服务实现跨平台，包括Mac，Linux，Android和Chrome OS\n",
    "\n",
    "另外，这是一个工业级强度的任务，做这种软件很难，更不用说让它完美，具有极快的速度。Kivy帮你在UI设计、图片下载和缓存处理方面省了很多精力，但也只能做这些了。\n",
    "\n",
    "所有，如果有些功能无法很快实现，请不要担心——错误和失败都是正常的，可以深入学习一些计算机间相互通信的知识。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "##总结"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这就远程桌面app的所有内容。这个app可以处理一下简单任务，比如点击iTunes里面的Play按钮，或者关闭一个程序。更复杂的任务，尤其是一些运维工作时，可能需要更专业的软件。\n",
    "\n",
    "我们用Flask建立了服务器实现了对图片的动态处理，主机系统的交互。我们还做了一个轻量级的JavaScript客户端实现了想要的功能，这说明我们的Kivy app并不是非主流方法。而且，在写Kivy代码之前，我们就有了服务器和客户端原型。\n",
    "\n",
    "这个原型帮助我们快速实现了我们的想法，可以立刻看到实现的效果。这里不打算讨论测试驱动开发（test-driven development，TDD），它是否成熟值得商榷，也不论事件驱动编程对这个案例是否有益。但是先做好每一个部分，然后把它们组合在一起，这种分而治之的方法还是比一下子写一个整体要更有效率。\n",
    "\n",
    "最后，Kivy也能很好的处理网络GUI app。比如，上一章里面与Twisted协作，实现通过网络加载内容的功能——这些都为建立多用户的网络app提供了极大帮助。\n",
    "\n",
    "现在，让我们进入另一个领域：Kivy游戏开发。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
